#include "UserDatabase.h"

#include <QMessageBox>

#include <random>

UserDatabase::UserDatabase()
{
    initDatabase();
}

int UserDatabase::addUser(QString username, QString password, User::UserType type)
{
    DatabaseLock lock(this);
    QSqlQuery query;

    // Check if user already exists
    query.prepare("SELECT userId FROM user_auth "
               "WHERE username=:username");
    query.bindValue(":username", QVariant(username));
    query.exec();

    // User already exists
    if (query.next())
        return USER_EXISTS;

    QString salt = generateSalt();
    QString hashedPassword = passwordHash(password, salt);

    // Insert new user record
    query.prepare("INSERT INTO user_auth (username, password, type, salt) "
                  "VALUES (:username, :password, :type, :salt)");
    query.bindValue(":username", QVariant(username));
    query.bindValue(":password", QVariant(hashedPassword));
    query.bindValue(":type", type);
    query.bindValue(":salt", QVariant(salt));
    query.exec();

    // Get generated user Id to return
    // NOTE: There is probably a more efficient way of doing this
    int id = getId(username, password);

    return id;
}

int UserDatabase::getId(QString username, QString password)
{
    // Make query to find username and password in database
    DatabaseLock lock(this);
    QSqlQuery query;

    // Get password salt from database
    query.prepare("SELECT salt FROM user_auth WHERE username=:username");
    query.bindValue(":username", username);
    query.exec();

    if (!query.next())
        return Database::FAILURE;

    QString salt = query.value(0).toString();

    // Compute password hash
    QString hashedPassword = passwordHash(password, salt);

    query.prepare("SELECT userId FROM user_auth "
               "WHERE username=:username "
               "AND password=:password");
    query.bindValue(":username", QVariant(username));
    query.bindValue(":password", QVariant(hashedPassword));

    // Something went wrong with the SQL query (outdated database?)
    if (!query.exec())
    {
        QMessageBox::warning(NULL, "Warning", "Invalid SQL query.");
        return Database::FAILURE;
    }

    // If nothing was returned in the query, the username and password
    // combination does not exist in the database
    if (!query.next())
        return Database::FAILURE;

    // Read user id from query
    int idPos = query.record().indexOf("userId");
    int userId = query.value(idPos).toInt();

    return userId;
}

User::UserType UserDatabase::getType(int userId)
{
    // Make query to find user id in database
    DatabaseLock lock(this);
    QSqlQuery query;

    query.prepare("SELECT type FROM user_auth "
               "WHERE userId=:userId ");
    query.bindValue(":userId", QVariant(userId));

    // Something went wrong with the SQL query (outdated database?)
    if (!query.exec())
    {
        QMessageBox::warning(NULL, "Warning", "Invalid SQL query.");
        return User::INVALID;
    }

    // If nothing was returned in the query,
    // the user id was not found in the database
    if (!query.next())
        return User::INVALID;

    // Read user type from query
    int typePos = query.record().indexOf("type");
    User::UserType type = static_cast<User::UserType>(query.value(typePos).toInt());

    return type;
}

QList<int> * UserDatabase::getUsersOfType(User::UserType type)
{
    userList.clear();

    DatabaseLock lock(this);
    QSqlQuery query;

    query.prepare("SELECT userId FROM user_auth WHERE type=:type");
    query.addBindValue((int)type);
    query.exec();

    while (query.next())
    {
        userList.push_back(query.record().value(0).toInt());
    }

    return &userList;
}

void UserDatabase::initDatabase()
{
    DatabaseLock lock(this);
    QSqlQuery query;

    // Check if user_auth table already exists
    query.exec("SELECT name FROM sqlite_master WHERE type='table' AND name = 'user_auth'");

    // If query returns a value, the table already exists and initialization can be skipped
    if (query.next())
        return;

    // Create table to hold user authentication data
    // Fields: userId, username, password, type
    query.exec("CREATE TABLE IF NOT EXISTS user_auth ("
               "userId INTEGER PRIMARY KEY AUTOINCREMENT, "
               "username varchar(255) NOT NULL UNIQUE, "
               "password char(64), "
               "type int, "
               "salt char(64))");

    // Insert default admin with username "admin" and password "12345"
    addUser("admin", "12345", User::ADMIN);
}

QString UserDatabase::passwordHash(const QString password, const QString salt) const
{
    // Compute password hash
    QString saltedPassword = salt + password;

    // Compute password hash
    QByteArray passwordHashRaw = QCryptographicHash::hash(saltedPassword.toLocal8Bit(),
                             QCryptographicHash::Sha256);
    QString passwordHash = passwordHashRaw.toHex();

    return passwordHash;
}

QString UserDatabase::generateSalt() const
{
    QByteArray randArray;

    // Generate a 256 bit number (from 8 random integers)
    for (int i = 0; i < 8; i++)
    {
        std::random_device rd;
        std::uniform_int_distribution<int> dist;

        int randInt = dist(rd);
        QByteArray randIntBytes = QByteArray::number(randInt, 16);

        randArray.append(randIntBytes);
    }

    return randArray;
}

UserDatabase::~UserDatabase()
{
    close();
}

void UserDatabase::resetPassword(QString username)
{
    DatabaseLock lock(this);
    QSqlQuery query;

    // Check if user exists
    query.prepare("SELECT userId, salt FROM user_auth "
                  "WHERE username=:username");
    query.bindValue(":username", QVariant(username));
    query.exec();

    // User found
    if (query.next())
    {
        QString salt = query.record().value("salt").toString();

        query.prepare("UPDATE user_auth SET password=:password "
                      "WHERE username=:username");
        query.bindValue(":password", QVariant(passwordHash("1234", salt)));
        query.bindValue(":username", QVariant(username));
        query.exec();
    }
}
